package pl.edu.mimuw.irs.core.model.simulation;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.model.analysis.History;
import pl.edu.mimuw.irs.core.model.analysis.Report;
import pl.edu.mimuw.irs.core.model.analysis.TPair;
import pl.edu.mimuw.irs.core.model.analysis.Time;
import pl.edu.mimuw.irs.core.model.analysis.atomic.ActivityPlace;
import pl.edu.mimuw.irs.core.model.analysis.resources.Fire;
import pl.edu.mimuw.irs.core.model.analysis.resources.RequirementPlace;
import pl.edu.mimuw.irs.core.model.analysis.resources.ResourcePlace;
import pl.edu.mimuw.irs.core.model.analysis.workflow.AnalysisWorkflow;
import pl.edu.mimuw.irs.core.model.design.atomic.AtomicSubnet;
import pl.edu.mimuw.irs.core.model.design.net.IRSActivity;
import pl.edu.mimuw.irs.core.model.design.net.IRSWorkflow;
import pl.edu.mimuw.irs.core.model.design.resource.Requirement;
import pl.edu.mimuw.irs.core.model.petrinet.IPlace;
import pl.edu.mimuw.irs.core.model.petrinet.Place;
import pl.edu.mimuw.irs.core.model.petrinet.Transition;
import pl.edu.mimuw.irs.core.model.petrinet.workflow.Workflow;
import pl.edu.mimuw.irs.core.model.simulation.atomic.EventPlace;
import pl.edu.mimuw.irs.core.model.simulation.atomic.SimulationNodeFactory;
import pl.edu.mimuw.irs.core.util.Converter;

public class SimulationWorkflow extends Workflow implements PropertyChangeListener {

	private static final long serialVersionUID = -8596955210932085012L;
	
	// *** Properties
	
	public static final String RUNS = "RUNS";
	public static final String FINISHED = "FINISHED";
	
	// *** Attributes

	private PropertyChangeSupport listeners;
	private AtomicSubnet atomicSubnet;
	private Report report;
	/* 
	 * Stores all currently applicable runs (all that begin with the
	 * same steps and hirings as manually chosen steps - constraints).
	 */
	private List<History> runs;
	private Map<ActivityPlace, Map<RequirementPlace, Set<ResourcePlace>>> selection;
	
	private Map<IRSActivity, TPair> hiringMap;
	private Map<Requirement, RequirementPlace> requirementsMap;
	private History manualRun;
	private int step;
	
	public Map<ActivityPlace, Map<RequirementPlace, Set<ResourcePlace>>> getSelection() {
		if (selection == null) 
			selection = new LinkedHashMap<ActivityPlace, Map<RequirementPlace, Set<ResourcePlace>>>();
		return selection;
	}
	public PropertyChangeSupport getListeners() {
		if (listeners == null) listeners = new PropertyChangeSupport(this);
		return listeners;
	}
	public Map<IRSActivity, TPair> getHiringMap() {
		if (hiringMap == null) hiringMap = 
			new LinkedHashMap<IRSActivity, TPair>();
		return hiringMap;
	}
	public Map<Requirement, RequirementPlace> getRequirementsMap() {
		if (requirementsMap == null) requirementsMap =
			new LinkedHashMap<Requirement, RequirementPlace>();
		return requirementsMap;
	}
	public History getManualRun() {
		if (manualRun == null) manualRun = new History();
		return manualRun;
	}
	public int getStep() {
		return step;
	}
	public List<History> getApplicableRuns() {
		if (report == null) return new LinkedList<History>();
		return report.getCustomRuns();
	}
	
	// *** Constructors
	
	public SimulationWorkflow(IRSWorkflow workflow,
			AnalysisWorkflow analysisWorkflow, 
			Report report) {
		atomicSubnet = workflow.getRootPlace().getAtomicWorkflow(
				new SimulationNodeFactory(getIdManager()));
		
		this.report = report;
		/* We will often modify this list so it must be a linked list */
		runs = new LinkedList<History>(report.getRuns());
		/* Clear any report custom data */
		report.setCustomRuns(runs);
		hiringMap = analysisWorkflow.getActivityHiringMap();
		requirementsMap = analysisWorkflow.getGraph().getRequirements();
		
		loadNet(atomicSubnet);
		attachListeners();
		getIn().setTokens(1);
	}
	
	// *** Methods
	
	private void attachListeners() {
		for (IPlace place : getPlaces()) {
			if (!(place instanceof EventPlace)) continue;
			EventPlace eventPlace = (EventPlace) place;
			eventPlace.addPropertyChangeListener(Place.TOKENS, this);
			eventPlace.addPropertyChangeListener(EventPlace.BUSY, this);
		}
		locateInOut();
		getOut().addPropertyChangeListener(this);
	}
	
	public Map<RequirementPlace, Set<ResourcePlace>> getPossibilities(IRSActivity activity) {
		Map<RequirementPlace, Set<ResourcePlace>> possibilities = 
			new LinkedHashMap<RequirementPlace, Set<ResourcePlace>>();
		
		for (History history : report.getCustomRuns()) {
			if (history.getOrder().size() <= getStep()) continue;
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
				history.getOrder().get(getStep());
			TPair pair = getHiringMap().get(activity);
			Map<ResourcePlace, Set<RequirementPlace>> involved = 
				events.get(pair.getHire());
			if (involved == null) continue;
			
			for (Entry<ResourcePlace, Set<RequirementPlace>> entry : 
					involved.entrySet()) {
				for (RequirementPlace requirement : entry.getValue()) {
					Set<ResourcePlace> resources = possibilities.get(requirement);
					if (resources == null) {
						resources = new LinkedHashSet<ResourcePlace>();
						possibilities.put(requirement, resources);
					}
					resources.add(entry.getKey());
				}
			}
		}
		
		return possibilities;
	}
	
	public List<History> getTmpRuns(IRSActivity activity, RequirementPlace requirement, ResourcePlace resource) {
		List<History> applicableRuns = new LinkedList<History>();
		
		for (History history : report.getCustomRuns()) {
			/*
			 * Get current events.
			 */
			if (history.getOrder().size() <= getStep()) continue;
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
				history.getOrder().get(getStep());
			if (events == null) continue;
			
			/*
			 * Take under consideration only those which apply to this activity.
			 */
			TPair pair = getHiringMap().get(activity);
			Map<ResourcePlace, Set<RequirementPlace>> involved = 
				events.get(pair.getHire());
			if (involved == null) continue;
			
			/*
			 * Get only those which uses given resource for given requirement.
			 */
			Set<RequirementPlace> requirements = involved.get(resource);
			if (requirements == null) continue;
			if (!requirements.contains(requirement)) continue;
			
			/*
			 * This history applies to our activity + requirement + resource.
			 * So we can get it for analysis.
			 */
			applicableRuns.add(history);
		}
		
		return applicableRuns;
	}

	public List<History> getRuns(History run) {
		List<History> applicableRuns = new LinkedList<History>();
		
		for (History history : runs) {
			/*
			 * If run applies to our history we add it to the result.
			 */
			if (history.beginsWith(run)) applicableRuns.add(history);
		}
		
		return applicableRuns;
	}
	
	public void select(ActivityPlace activityPlace,
			RequirementPlace requirement, ResourcePlace resource) {
		List<History> oldRuns = report.getCustomRuns();
		
		/* Check current selection */
		Map<RequirementPlace, Set<ResourcePlace>> selectedRequirements = 
			getSelection().get(activityPlace);
		if (selectedRequirements == null) {
			selectedRequirements = 
				new LinkedHashMap<RequirementPlace, Set<ResourcePlace>>();
			getSelection().put(activityPlace, selectedRequirements);
		}
		Set<ResourcePlace> selectedResources = 
			selectedRequirements.get(requirement);
		if (selectedResources == null) {
			selectedResources = 
				new LinkedHashSet<ResourcePlace>();
			selectedRequirements.put(requirement, selectedResources);
		}
		// TODO committedRequirements - Should handle double - but it does not.
		if (selectedResources.size() > 0) selectedResources.clear();
		if (resource != null) selectedResources.add(resource);
		if (selectedResources.size() == 0) selectedRequirements.remove(requirement);

		activityPlace.setTokens(selectedRequirements.size());
		
		setSelectionRuns();
		//report.setCustomRuns(getTmpRuns(activityPlace.getRequirement().getActivity(), 
		//		requirement, resource));
		
		getListeners().firePropertyChange(RUNS, oldRuns, report.getCustomRuns());
	}
	
	private void setSelectionRuns() {
		
		report.setCustomRuns(new LinkedList<History>(this.runs));
		
		/* Apply all selections to commited run */
		for (Entry<ActivityPlace, Map<RequirementPlace, Set<ResourcePlace>>> activityEntry : getSelection().entrySet()) {
			/* All are EventPlaces but to make it neater we will check it anyway */
			if (activityEntry.getKey() instanceof EventPlace) {
				/* Omit selection if place is already busy. */
				EventPlace eventPlace = (EventPlace) activityEntry.getKey();
				if (eventPlace.isBusy()) continue;
			}
			for (Entry<RequirementPlace, Set<ResourcePlace>> requirementEntry : activityEntry.getValue().entrySet()) {
				for (ResourcePlace resource : requirementEntry.getValue()) {
					report.setCustomRuns(getTmpRuns(
							activityEntry.getKey().getRequirement().getActivity(), 
							requirementEntry.getKey(), 
							resource));
				}
			}
		}
	}
	
	public Map<RequirementPlace, Set<ResourcePlace>> getPossibilities(ActivityPlace place) {
		IRSActivity activity = place.getRequirement().getActivity();
		
		return getPossibilities(activity);
	}
	
	public Calculations getCalculations(ActivityPlace activityPlace,
			RequirementPlace requirement, ResourcePlace resource) {
		List<History> applicableRuns = 
				getTmpRuns(activityPlace.getRequirement().getActivity(), 
						requirement, resource);
		return new Calculations(applicableRuns);
	}
	
	/* 
	 * This method assumes that run begins with manualRun and does not check
	 * it.
	 */
	private int getCurrentStep(History run, History manualRun) {
		if (manualRun.getOrder().size() == 0) return 0;
		int step = manualRun.getOrder().size() - 1;
		Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
			run.getOrder().get(step);
		Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> manualEvents = 
			manualRun.getOrder().get(step);
		/* If events is null we want NPE to be thrown. */
		
		/* If some events have not been started - the step does not change. */
		if (events.size() != manualEvents.size()) return step;
		for (Entry<Transition, Map<ResourcePlace, Set<RequirementPlace>>> event : 
				events.entrySet()) {
			Map<ResourcePlace, Set<RequirementPlace>> resources = 
					manualEvents.get(event.getKey());
			/* Resources map must not be empty - if is - we want NPE to be thrown. */
			for (Entry<ResourcePlace, Set<RequirementPlace>> entry : event.getValue().entrySet()) {
				Set<RequirementPlace> requirements = 
						resources.get(entry.getKey());
				if (requirements == null || 
						requirements.size() != entry.getValue().size())
					return step;
				if (!requirements.containsAll(entry.getValue())) return step;
			}
		}
		
		/* 
		 * If we are here it means, that all requirements of this timestamp 
		 * has been fulfilled. So the next step is just step + 1.
		 */
		return step + 1;
	}
	
	public void addPropertyChangeListener(PropertyChangeListener listener) {
		getListeners().addPropertyChangeListener(listener);
	}

	public void removePropertyChangeListener(PropertyChangeListener listener) {
		getListeners().removePropertyChangeListener(listener);
	}

	@Override
	public void propertyChange(PropertyChangeEvent arg0) {
		/* Token on out place functionality */
		if (arg0.getPropertyName() == Place.TOKENS &&
				arg0.getSource() == getOut()) handleOutMarked();
		
		/* Standard functionality */
		if (arg0.getPropertyName() != EventPlace.BUSY) return;
		if (!(arg0.getSource() instanceof ActivityPlace)) return;
		ActivityPlace activityPlace = (ActivityPlace) arg0.getSource();
		
		/* Check if it was hire or fire */
		IRSActivity irsActivity = activityPlace.getRequirement().getActivity();
		//Boolean oldValue = (Boolean) arg0.getOldValue();
		Boolean newValue = (Boolean) arg0.getNewValue();
		Transition event;
		if (newValue == true) event = getHiringMap().get(irsActivity).getHire();
		else event = getHiringMap().get(irsActivity).getFire();
		
		//for (History run : runs) {
		for (History run : report.getCustomRuns()) {
			int step = getCurrentStep(run, getManualRun());
			if (run.getOrder().size() <= step) continue;
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
				run.getOrder().get(step);
			if (events == null) continue;
			if (!events.containsKey(event)) continue;
			
			/*
			 * Here we know we found corresponding event - 
			 * we can get selection. We can get it from event
			 * because it is exactly the same as ours.
			 */
			Map<RequirementPlace, Set<ResourcePlace>> selection = 
				getSelection().get(activityPlace);
//			Map<ResourcePlace, Set<RequirementPlace>> selection = 
//				events.get(event);
			if (selection == null) selection = Collections.emptyMap();
			Map<ResourcePlace, Set<RequirementPlace>> swapped = 
				Converter.swap(selection);
			/*
			 * Mark activity execution.
			 */
			markExecution(step, event, swapped);
//			markExecution(step, event, selection);
			return;
		}
		
		//throw new NoApplicableRunsFound(getManualRun());
		this.runs = Collections.emptyList();
		List<History> oldRuns = report.getCustomRuns();
		report.setCustomRuns(runs);
		clearSelection();
		getListeners().firePropertyChange(RUNS, oldRuns, report.getCustomRuns());
	}
	
	private void handleOutMarked() {
		/* Remove all longer runs. We assume that because of non-zero activity length if history
		 * is equal in length to our custom run and it is comparable with it, then it is the same. */
		List<History> finalRuns = new LinkedList<History>();
		for (History run : runs) {
			if (run.getOrder().size() == getManualRun().getOrder().size()) finalRuns.add(run);
		}
		report.setCustomRuns(finalRuns);
		
		getListeners().firePropertyChange(FINISHED, null, getOut());
	}
	
	private void markExecution(int step, Transition event,
			Map<ResourcePlace, Set<RequirementPlace>> swapped) {
		this.getManualRun().addEvent(new Time(step), event, swapped);
		this.runs = getRuns(getManualRun());
		List<History> oldRuns = report.getCustomRuns();
		this.step = step;
		/* Clear selection when time changed */
		if (event instanceof Fire) {
			report.setCustomRuns(runs);
			clearSelection();
		}
		//this.timestamp = currentTime;
		getListeners().firePropertyChange(RUNS, oldRuns, report.getCustomRuns());
	}
	
	/** Clear selection only for not busy places */
	private void clearSelection() {
		Set<EventPlace> placesToClear = new LinkedHashSet<EventPlace>();
		for (IPlace place : getPlaces()) {
			/* Clear only activity places selections */
			if (!(place instanceof EventPlace)) continue;
			EventPlace eventPlace = (EventPlace) place;
			/* If place is busy we do not want to clear its selection */
			if (eventPlace.isBusy()) continue;
			placesToClear.add(eventPlace);
		}
		for (EventPlace eventPlace : placesToClear) {
			getSelection().remove(eventPlace);
			eventPlace.setTokens(0);
		}
	}
}
